40223e011f8d06c35f4319751c1bc9695127ebba
[juce.git] /
1 /*\r
2   ==============================================================================\r
3 \r
4    This file is part of the JUCE library.\r
5    Copyright (c) 2017 - ROLI Ltd.\r
6 \r
7    JUCE is an open source library subject to commercial or open-source\r
8    licensing.\r
9 \r
10    The code included in this file is provided under the terms of the ISC license\r
11    http://www.isc.org/downloads/software-support-policy/isc-license. Permission\r
12    To use, copy, modify, and/or distribute this software for any purpose with or\r
13    without fee is hereby granted provided that the above copyright notice and\r
14    this permission notice appear in all copies.\r
15 \r
16    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER\r
17    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE\r
18    DISCLAIMED.\r
19 \r
20   ==============================================================================\r
21 */\r
22 \r
23 package com.juce.networkgraphicsdemo;\r
24 \r
25 import android.app.Activity;\r
26 import android.app.AlertDialog;\r
27 import android.content.DialogInterface;\r
28 import android.content.Context;\r
29 import android.content.Intent;\r
30 import android.content.res.Configuration;\r
31 import android.content.pm.PackageInfo;\r
32 import android.content.pm.PackageManager;\r
33 import android.net.http.SslError;\r
34 import android.net.Uri;\r
35 import android.os.Bundle;\r
36 import android.os.Looper;\r
37 import android.os.Handler;\r
38 import android.os.Message;\r
39 import android.os.ParcelUuid;\r
40 import android.os.Environment;\r
41 import android.view.*;\r
42 import android.view.inputmethod.BaseInputConnection;\r
43 import android.view.inputmethod.EditorInfo;\r
44 import android.view.inputmethod.InputConnection;\r
45 import android.view.inputmethod.InputMethodManager;\r
46 import android.graphics.*;\r
47 import android.text.ClipboardManager;\r
48 import android.text.InputType;\r
49 import android.util.DisplayMetrics;\r
50 import android.util.Log;\r
51 import android.util.Pair;\r
52 import android.webkit.SslErrorHandler;\r
53 import android.webkit.WebChromeClient;\r
54 import android.webkit.WebView;\r
55 import android.webkit.WebViewClient;\r
56 import java.lang.Runnable;\r
57 import java.lang.ref.WeakReference;\r
58 import java.lang.reflect.*;\r
59 import java.util.*;\r
60 import java.io.*;\r
61 import java.net.URL;\r
62 import java.net.HttpURLConnection;\r
63 import android.media.AudioManager;\r
64 import android.Manifest;\r
65 import java.util.concurrent.CancellationException;\r
66 import java.util.concurrent.Future;\r
67 import java.util.concurrent.Executors;\r
68 import java.util.concurrent.ExecutorService;\r
69 import java.util.concurrent.ExecutionException;\r
70 import java.util.concurrent.TimeUnit;\r
71 import java.util.concurrent.Callable;\r
72 import java.util.concurrent.TimeoutException;\r
73 import java.util.concurrent.locks.ReentrantLock;\r
74 import java.util.concurrent.atomic.*;\r
75 \r
76 \r
77 \r
78 //==============================================================================\r
79 public class JUCENetworkGraphicsDemo   extends Activity\r
80 {\r
81     //==============================================================================\r
82     static\r
83     {\r
84         System.loadLibrary ("juce_jni");\r
85     }\r
86 \r
87     //==============================================================================\r
88     public boolean isPermissionDeclaredInManifest (int permissionID)\r
89     {\r
90         return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID));\r
91     }\r
92 \r
93     public boolean isPermissionDeclaredInManifest (String permissionToCheck)\r
94     {\r
95         try\r
96         {\r
97             PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS);\r
98 \r
99             if (info.requestedPermissions != null)\r
100                 for (String permission : info.requestedPermissions)\r
101                     if (permission.equals (permissionToCheck))\r
102                         return true;\r
103         }\r
104         catch (PackageManager.NameNotFoundException e)\r
105         {\r
106             Log.d ("JUCE", "isPermissionDeclaredInManifest: PackageManager.NameNotFoundException = " + e.toString());\r
107         }\r
108 \r
109         Log.d ("JUCE", "isPermissionDeclaredInManifest: could not find requested permission " + permissionToCheck);\r
110         return false;\r
111     }\r
112 \r
113     //==============================================================================\r
114     // these have to match the values of enum PermissionID in C++ class RuntimePermissions:\r
115     private static final int JUCE_PERMISSIONS_RECORD_AUDIO = 1;\r
116     private static final int JUCE_PERMISSIONS_BLUETOOTH_MIDI = 2;\r
117     private static final int JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE = 3;\r
118     private static final int JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 4;\r
119     private static final int JUCE_PERMISSIONS_CAMERA = 5;\r
120 \r
121     private static String getAndroidPermissionName (int permissionID)\r
122     {\r
123         switch (permissionID)\r
124         {\r
125             case JUCE_PERMISSIONS_RECORD_AUDIO:           return Manifest.permission.RECORD_AUDIO;\r
126             case JUCE_PERMISSIONS_BLUETOOTH_MIDI:         return Manifest.permission.ACCESS_COARSE_LOCATION;\r
127                                                           // use string value as this is not defined in SDKs < 16\r
128             case JUCE_PERMISSIONS_READ_EXTERNAL_STORAGE:  return "android.permission.READ_EXTERNAL_STORAGE";\r
129             case JUCE_PERMISSIONS_WRITE_EXTERNAL_STORAGE: return Manifest.permission.WRITE_EXTERNAL_STORAGE;\r
130             case JUCE_PERMISSIONS_CAMERA:                 return Manifest.permission.CAMERA;\r
131         }\r
132 \r
133         // unknown permission ID!\r
134         assert false;\r
135         return new String();\r
136     }\r
137 \r
138     public boolean isPermissionGranted (int permissionID)\r
139     {\r
140         return getApplicationContext().checkCallingOrSelfPermission (getAndroidPermissionName (permissionID)) == PackageManager.PERMISSION_GRANTED;\r
141     }\r
142 \r
143     private Map<Integer, Long> permissionCallbackPtrMap;\r
144 \r
145     public void requestRuntimePermission (int permissionID, long ptrToCallback)\r
146     {\r
147         String permissionName = getAndroidPermissionName (permissionID);\r
148 \r
149         if (getApplicationContext().checkCallingOrSelfPermission (permissionName) != PackageManager.PERMISSION_GRANTED)\r
150         {\r
151             // remember callbackPtr, request permissions, and let onRequestPermissionResult call callback asynchronously\r
152             permissionCallbackPtrMap.put (permissionID, ptrToCallback);\r
153             requestPermissionsCompat (new String[]{permissionName}, permissionID);\r
154         }\r
155         else\r
156         {\r
157             // permissions were already granted before, we can call callback directly\r
158             androidRuntimePermissionsCallback (true, ptrToCallback);\r
159         }\r
160     }\r
161 \r
162     private native void androidRuntimePermissionsCallback (boolean permissionWasGranted, long ptrToCallback);\r
163 \r
164 \r
165     //==============================================================================\r
166     public interface JuceMidiPort\r
167     {\r
168         boolean isInputPort();\r
169 \r
170         // start, stop does nothing on an output port\r
171         void start();\r
172         void stop();\r
173 \r
174         void close();\r
175 \r
176         // send will do nothing on an input port\r
177         void sendMidi (byte[] msg, int offset, int count);\r
178     }\r
179 \r
180     //==============================================================================\r
181     //==============================================================================\r
182     public class BluetoothManager\r
183     {\r
184         BluetoothManager()\r
185         {\r
186         }\r
187 \r
188         public String[] getMidiBluetoothAddresses()\r
189         {\r
190             String[] bluetoothAddresses = new String[0];\r
191             return bluetoothAddresses;\r
192         }\r
193 \r
194         public String getHumanReadableStringForBluetoothAddress (String address)\r
195         {\r
196             return address;\r
197         }\r
198 \r
199         public int getBluetoothDeviceStatus (String address)\r
200         {\r
201             return 0;\r
202         }\r
203 \r
204         public void startStopScan (boolean shouldStart)\r
205         {\r
206         }\r
207 \r
208         public boolean pairBluetoothMidiDevice(String address)\r
209         {\r
210             return false;\r
211         }\r
212 \r
213         public void unpairBluetoothMidiDevice (String address)\r
214         {\r
215         }\r
216     }\r
217 \r
218     //==============================================================================\r
219     public class MidiDeviceManager\r
220     {\r
221         public MidiDeviceManager()\r
222         {\r
223         }\r
224 \r
225         public String[] getJuceAndroidMidiInputDevices()\r
226         {\r
227             return new String[0];\r
228         }\r
229 \r
230         public String[] getJuceAndroidMidiOutputDevices()\r
231         {\r
232             return new String[0];\r
233         }\r
234 \r
235         public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host)\r
236         {\r
237             return null;\r
238         }\r
239 \r
240         public JuceMidiPort openMidiOutputPortWithJuceIndex (int index)\r
241         {\r
242             return null;\r
243         }\r
244 \r
245         public String getInputPortNameForJuceIndex (int index)\r
246         {\r
247             return "";\r
248         }\r
249 \r
250         public String getOutputPortNameForJuceIndex (int index)\r
251         {\r
252             return "";\r
253         }\r
254     }\r
255 \r
256 \r
257     public MidiDeviceManager getAndroidMidiDeviceManager()\r
258     {\r
259         return null;\r
260     }\r
261 \r
262     public BluetoothManager getAndroidBluetoothManager()\r
263     {\r
264         return null;\r
265     }\r
266 \r
267     //==============================================================================\r
268     @Override\r
269     public void onCreate (Bundle savedInstanceState)\r
270     {\r
271         super.onCreate (savedInstanceState);\r
272 \r
273         isScreenSaverEnabled = true;\r
274         hideActionBar();\r
275         viewHolder = new ViewHolder (this);\r
276         setContentView (viewHolder);\r
277 \r
278         setVolumeControlStream (AudioManager.STREAM_MUSIC);\r
279 \r
280         permissionCallbackPtrMap = new HashMap<Integer, Long>();\r
281         appPausedResumedListeners = new HashMap<Long, AppPausedResumedListener>();\r
282     }\r
283 \r
284     @Override\r
285     protected void onDestroy()\r
286     {\r
287         quitApp();\r
288         super.onDestroy();\r
289 \r
290         clearDataCache();\r
291     }\r
292 \r
293     @Override\r
294     protected void onPause()\r
295     {\r
296         suspendApp();\r
297 \r
298         Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]);\r
299 \r
300         for (Long k : keys)\r
301             appPausedResumedListeners.get (k).appPaused();\r
302 \r
303         try\r
304         {\r
305             Thread.sleep (1000); // This is a bit of a hack to avoid some hard-to-track-down\r
306                                  // openGL glitches when pausing/resuming apps..\r
307         } catch (InterruptedException e) {}\r
308 \r
309         super.onPause();\r
310     }\r
311 \r
312     @Override\r
313     protected void onResume()\r
314     {\r
315         super.onResume();\r
316         resumeApp();\r
317 \r
318         Long[] keys = appPausedResumedListeners.keySet().toArray (new Long[appPausedResumedListeners.keySet().size()]);\r
319 \r
320         for (Long k : keys)\r
321             appPausedResumedListeners.get (k).appResumed();\r
322     }\r
323 \r
324     @Override\r
325     public void onConfigurationChanged (Configuration cfg)\r
326     {\r
327         super.onConfigurationChanged (cfg);\r
328         setContentView (viewHolder);\r
329     }\r
330 \r
331     private void callAppLauncher()\r
332     {\r
333         launchApp (getApplicationInfo().publicSourceDir,\r
334                    getApplicationInfo().dataDir);\r
335     }\r
336 \r
337     // Need to override this as the default implementation always finishes the activity.\r
338     @Override\r
339     public void onBackPressed()\r
340     {\r
341         ComponentPeerView focusedView = getViewWithFocusOrDefaultView();\r
342 \r
343         if (focusedView == null)\r
344             return;\r
345 \r
346         focusedView.backButtonPressed();\r
347     }\r
348 \r
349     private ComponentPeerView getViewWithFocusOrDefaultView()\r
350     {\r
351         for (int i = 0; i < viewHolder.getChildCount(); ++i)\r
352         {\r
353             if (viewHolder.getChildAt (i).hasFocus())\r
354                 return (ComponentPeerView) viewHolder.getChildAt (i);\r
355         }\r
356 \r
357         if (viewHolder.getChildCount() > 0)\r
358             return (ComponentPeerView) viewHolder.getChildAt (0);\r
359 \r
360         return null;\r
361     }\r
362 \r
363     //==============================================================================\r
364     private void hideActionBar()\r
365     {\r
366         // get "getActionBar" method\r
367         java.lang.reflect.Method getActionBarMethod = null;\r
368         try\r
369         {\r
370             getActionBarMethod = this.getClass().getMethod ("getActionBar");\r
371         }\r
372         catch (SecurityException e)     { return; }\r
373         catch (NoSuchMethodException e) { return; }\r
374         if (getActionBarMethod == null) return;\r
375 \r
376         // invoke "getActionBar" method\r
377         Object actionBar = null;\r
378         try\r
379         {\r
380             actionBar = getActionBarMethod.invoke (this);\r
381         }\r
382         catch (java.lang.IllegalArgumentException e) { return; }\r
383         catch (java.lang.IllegalAccessException e) { return; }\r
384         catch (java.lang.reflect.InvocationTargetException e) { return; }\r
385         if (actionBar == null) return;\r
386 \r
387         // get "hide" method\r
388         java.lang.reflect.Method actionBarHideMethod = null;\r
389         try\r
390         {\r
391             actionBarHideMethod = actionBar.getClass().getMethod ("hide");\r
392         }\r
393         catch (SecurityException e)     { return; }\r
394         catch (NoSuchMethodException e) { return; }\r
395         if (actionBarHideMethod == null) return;\r
396 \r
397         // invoke "hide" method\r
398         try\r
399         {\r
400             actionBarHideMethod.invoke (actionBar);\r
401         }\r
402         catch (java.lang.IllegalArgumentException e) {}\r
403         catch (java.lang.IllegalAccessException e) {}\r
404         catch (java.lang.reflect.InvocationTargetException e) {}\r
405     }\r
406 \r
407     void requestPermissionsCompat (String[] permissions, int requestCode)\r
408     {\r
409         Method requestPermissionsMethod = null;\r
410         try\r
411         {\r
412             requestPermissionsMethod = this.getClass().getMethod ("requestPermissions",\r
413                                                                   String[].class, int.class);\r
414         }\r
415         catch (SecurityException e)     { return; }\r
416         catch (NoSuchMethodException e) { return; }\r
417         if (requestPermissionsMethod == null) return;\r
418 \r
419         try\r
420         {\r
421             requestPermissionsMethod.invoke (this, permissions, requestCode);\r
422         }\r
423         catch (java.lang.IllegalArgumentException e) {}\r
424         catch (java.lang.IllegalAccessException e) {}\r
425         catch (java.lang.reflect.InvocationTargetException e) {}\r
426     }\r
427 \r
428     //==============================================================================\r
429     private native void launchApp (String appFile, String appDataDir);\r
430     private native void quitApp();\r
431     private native void suspendApp();\r
432     private native void resumeApp();\r
433     private native void setScreenSize (int screenWidth, int screenHeight, int dpi);\r
434     private native void appActivityResult (int requestCode, int resultCode, Intent data);\r
435     private native void appNewIntent (Intent intent);\r
436 \r
437     //==============================================================================\r
438     private ViewHolder viewHolder;\r
439     private MidiDeviceManager midiDeviceManager = null;\r
440     private BluetoothManager bluetoothManager = null;\r
441     private boolean isScreenSaverEnabled;\r
442     private java.util.Timer keepAliveTimer;\r
443 \r
444     public final ComponentPeerView createNewView (boolean opaque, long host)\r
445     {\r
446         ComponentPeerView v = new ComponentPeerView (this, opaque, host);\r
447         viewHolder.addView (v);\r
448         addAppPausedResumedListener (v, host);\r
449         return v;\r
450     }\r
451 \r
452     public final void deleteView (ComponentPeerView view)\r
453     {\r
454         removeAppPausedResumedListener (view, view.host);\r
455 \r
456         view.host = 0;\r
457 \r
458         ViewGroup group = (ViewGroup) (view.getParent());\r
459 \r
460         if (group != null)\r
461             group.removeView (view);\r
462     }\r
463 \r
464     public final void deleteNativeSurfaceView (NativeSurfaceView view)\r
465     {\r
466         ViewGroup group = (ViewGroup) (view.getParent());\r
467 \r
468         if (group != null)\r
469             group.removeView (view);\r
470     }\r
471 \r
472     final class ViewHolder  extends ViewGroup\r
473     {\r
474         public ViewHolder (Context context)\r
475         {\r
476             super (context);\r
477             setDescendantFocusability (ViewGroup.FOCUS_AFTER_DESCENDANTS);\r
478             setFocusable (false);\r
479         }\r
480 \r
481         protected final void onLayout (boolean changed, int left, int top, int right, int bottom)\r
482         {\r
483             setScreenSize (getWidth(), getHeight(), getDPI());\r
484 \r
485             if (isFirstResize)\r
486             {\r
487                 isFirstResize = false;\r
488                 callAppLauncher();\r
489             }\r
490         }\r
491 \r
492         private final int getDPI()\r
493         {\r
494             DisplayMetrics metrics = new DisplayMetrics();\r
495             getWindowManager().getDefaultDisplay().getMetrics (metrics);\r
496             return metrics.densityDpi;\r
497         }\r
498 \r
499         private boolean isFirstResize = true;\r
500     }\r
501 \r
502     public final void excludeClipRegion (android.graphics.Canvas canvas, float left, float top, float right, float bottom)\r
503     {\r
504         canvas.clipRect (left, top, right, bottom, android.graphics.Region.Op.DIFFERENCE);\r
505     }\r
506 \r
507     //==============================================================================\r
508     public final void setScreenSaver (boolean enabled)\r
509     {\r
510         if (isScreenSaverEnabled != enabled)\r
511         {\r
512             isScreenSaverEnabled = enabled;\r
513 \r
514             if (keepAliveTimer != null)\r
515             {\r
516                 keepAliveTimer.cancel();\r
517                 keepAliveTimer = null;\r
518             }\r
519 \r
520             if (enabled)\r
521             {\r
522                 getWindow().clearFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\r
523             }\r
524             else\r
525             {\r
526                 getWindow().addFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\r
527 \r
528                 // If no user input is received after about 3 seconds, the OS will lower the\r
529                 // task's priority, so this timer forces it to be kept active.\r
530                 keepAliveTimer = new java.util.Timer();\r
531 \r
532                 keepAliveTimer.scheduleAtFixedRate (new TimerTask()\r
533                 {\r
534                     @Override\r
535                     public void run()\r
536                     {\r
537                         android.app.Instrumentation instrumentation = new android.app.Instrumentation();\r
538 \r
539                         try\r
540                         {\r
541                             instrumentation.sendKeyDownUpSync (KeyEvent.KEYCODE_UNKNOWN);\r
542                         }\r
543                         catch (Exception e)\r
544                         {\r
545                         }\r
546                     }\r
547                 }, 2000, 2000);\r
548             }\r
549         }\r
550     }\r
551 \r
552     public final boolean getScreenSaver()\r
553     {\r
554         return isScreenSaverEnabled;\r
555     }\r
556 \r
557     //==============================================================================\r
558     public final String getClipboardContent()\r
559     {\r
560         ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE);\r
561 \r
562         CharSequence content = clipboard.getText();\r
563         return content != null ? content.toString() : new String();\r
564     }\r
565 \r
566     public final void setClipboardContent (String newText)\r
567     {\r
568         ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE);\r
569         clipboard.setText (newText);\r
570     }\r
571 \r
572     //==============================================================================\r
573     public final void showMessageBox (String title, String message, final long callback)\r
574     {\r
575         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
576         builder.setTitle (title)\r
577                .setMessage (message)\r
578                .setCancelable (true)\r
579                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
580                     {\r
581                         public void onCancel (DialogInterface dialog)\r
582                         {\r
583                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
584                         }\r
585                     })\r
586                .setPositiveButton ("OK", new DialogInterface.OnClickListener()\r
587                     {\r
588                         public void onClick (DialogInterface dialog, int id)\r
589                         {\r
590                             dialog.dismiss();\r
591                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
592                         }\r
593                     });\r
594 \r
595         builder.create().show();\r
596     }\r
597 \r
598     public final void showOkCancelBox (String title, String message, final long callback,\r
599                                        String okButtonText, String cancelButtonText)\r
600     {\r
601         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
602         builder.setTitle (title)\r
603                .setMessage (message)\r
604                .setCancelable (true)\r
605                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
606                     {\r
607                         public void onCancel (DialogInterface dialog)\r
608                         {\r
609                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
610                         }\r
611                     })\r
612                .setPositiveButton (okButtonText.isEmpty() ? "OK" : okButtonText, new DialogInterface.OnClickListener()\r
613                     {\r
614                         public void onClick (DialogInterface dialog, int id)\r
615                         {\r
616                             dialog.dismiss();\r
617                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 1);\r
618                         }\r
619                     })\r
620                .setNegativeButton (cancelButtonText.isEmpty() ? "Cancel" : cancelButtonText, new DialogInterface.OnClickListener()\r
621                     {\r
622                         public void onClick (DialogInterface dialog, int id)\r
623                         {\r
624                             dialog.dismiss();\r
625                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
626                         }\r
627                     });\r
628 \r
629         builder.create().show();\r
630     }\r
631 \r
632     public final void showYesNoCancelBox (String title, String message, final long callback)\r
633     {\r
634         AlertDialog.Builder builder = new AlertDialog.Builder (this);\r
635         builder.setTitle (title)\r
636                .setMessage (message)\r
637                .setCancelable (true)\r
638                .setOnCancelListener (new DialogInterface.OnCancelListener()\r
639                     {\r
640                         public void onCancel (DialogInterface dialog)\r
641                         {\r
642                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
643                         }\r
644                     })\r
645                .setPositiveButton ("Yes", new DialogInterface.OnClickListener()\r
646                     {\r
647                         public void onClick (DialogInterface dialog, int id)\r
648                         {\r
649                             dialog.dismiss();\r
650                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 1);\r
651                         }\r
652                     })\r
653                .setNegativeButton ("No", new DialogInterface.OnClickListener()\r
654                     {\r
655                         public void onClick (DialogInterface dialog, int id)\r
656                         {\r
657                             dialog.dismiss();\r
658                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 2);\r
659                         }\r
660                     })\r
661                .setNeutralButton ("Cancel", new DialogInterface.OnClickListener()\r
662                     {\r
663                         public void onClick (DialogInterface dialog, int id)\r
664                         {\r
665                             dialog.dismiss();\r
666                             JUCENetworkGraphicsDemo.this.alertDismissed (callback, 0);\r
667                         }\r
668                     });\r
669 \r
670         builder.create().show();\r
671     }\r
672 \r
673     public native void alertDismissed (long callback, int id);\r
674 \r
675     //==============================================================================\r
676     public interface AppPausedResumedListener\r
677     {\r
678         void appPaused();\r
679         void appResumed();\r
680     }\r
681 \r
682     private Map<Long, AppPausedResumedListener> appPausedResumedListeners;\r
683 \r
684     public void addAppPausedResumedListener (AppPausedResumedListener l, long listenerHost)\r
685     {\r
686         appPausedResumedListeners.put (new Long (listenerHost), l);\r
687     }\r
688 \r
689     public void removeAppPausedResumedListener (AppPausedResumedListener l, long listenerHost)\r
690     {\r
691         appPausedResumedListeners.remove (new Long (listenerHost));\r
692     }\r
693 \r
694     //==============================================================================\r
695     public final class ComponentPeerView extends ViewGroup\r
696                                          implements View.OnFocusChangeListener, AppPausedResumedListener\r
697     {\r
698         public ComponentPeerView (Context context, boolean opaque_, long host)\r
699         {\r
700             super (context);\r
701             this.host = host;\r
702             setWillNotDraw (false);\r
703             opaque = opaque_;\r
704 \r
705             setFocusable (true);\r
706             setFocusableInTouchMode (true);\r
707             setOnFocusChangeListener (this);\r
708 \r
709             // swap red and blue colours to match internal opengl texture format\r
710             ColorMatrix colorMatrix = new ColorMatrix();\r
711 \r
712             float[] colorTransform = { 0,    0,    1.0f, 0,    0,\r
713                                        0,    1.0f, 0,    0,    0,\r
714                                        1.0f, 0,    0,    0,    0,\r
715                                        0,    0,    0,    1.0f, 0 };\r
716 \r
717             colorMatrix.set (colorTransform);\r
718             paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix));\r
719 \r
720             java.lang.reflect.Method method = null;\r
721 \r
722             try\r
723             {\r
724                 method = getClass().getMethod ("setLayerType", int.class, Paint.class);\r
725             }\r
726             catch (SecurityException e)     {}\r
727             catch (NoSuchMethodException e) {}\r
728 \r
729             if (method != null)\r
730             {\r
731                 try\r
732                 {\r
733                     int layerTypeNone = 0;\r
734                     method.invoke (this, layerTypeNone, null);\r
735                 }\r
736                 catch (java.lang.IllegalArgumentException e) {}\r
737                 catch (java.lang.IllegalAccessException e) {}\r
738                 catch (java.lang.reflect.InvocationTargetException e) {}\r
739             }\r
740         }\r
741 \r
742         //==============================================================================\r
743         private native void handlePaint (long host, Canvas canvas, Paint paint);\r
744 \r
745         @Override\r
746         public void onDraw (Canvas canvas)\r
747         {\r
748             if (host == 0)\r
749                 return;\r
750 \r
751             handlePaint (host, canvas, paint);\r
752         }\r
753 \r
754         @Override\r
755         public boolean isOpaque()\r
756         {\r
757             return opaque;\r
758         }\r
759 \r
760         private boolean opaque;\r
761         private long host;\r
762         private Paint paint = new Paint();\r
763 \r
764         //==============================================================================\r
765         private native void handleMouseDown (long host, int index, float x, float y, long time);\r
766         private native void handleMouseDrag (long host, int index, float x, float y, long time);\r
767         private native void handleMouseUp   (long host, int index, float x, float y, long time);\r
768 \r
769         @Override\r
770         public boolean onTouchEvent (MotionEvent event)\r
771         {\r
772             if (host == 0)\r
773                 return false;\r
774 \r
775             int action = event.getAction();\r
776             long time = event.getEventTime();\r
777 \r
778             switch (action & MotionEvent.ACTION_MASK)\r
779             {\r
780                 case MotionEvent.ACTION_DOWN:\r
781                     handleMouseDown (host, event.getPointerId(0), event.getX(), event.getY(), time);\r
782                     return true;\r
783 \r
784                 case MotionEvent.ACTION_CANCEL:\r
785                 case MotionEvent.ACTION_UP:\r
786                     handleMouseUp (host, event.getPointerId(0), event.getX(), event.getY(), time);\r
787                     return true;\r
788 \r
789                 case MotionEvent.ACTION_MOVE:\r
790                 {\r
791                     int n = event.getPointerCount();\r
792                     for (int i = 0; i < n; ++i)\r
793                         handleMouseDrag (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
794 \r
795                     return true;\r
796                 }\r
797 \r
798                 case MotionEvent.ACTION_POINTER_UP:\r
799                 {\r
800                     int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;\r
801                     handleMouseUp (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
802                     return true;\r
803                 }\r
804 \r
805                 case MotionEvent.ACTION_POINTER_DOWN:\r
806                 {\r
807                     int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;\r
808                     handleMouseDown (host, event.getPointerId(i), event.getX(i), event.getY(i), time);\r
809                     return true;\r
810                 }\r
811 \r
812                 default:\r
813                     break;\r
814             }\r
815 \r
816             return false;\r
817         }\r
818 \r
819         //==============================================================================\r
820         private native void handleKeyDown (long host, int keycode, int textchar);\r
821         private native void handleKeyUp (long host, int keycode, int textchar);\r
822         private native void handleBackButton (long host);\r
823         private native void handleKeyboardHidden (long host);\r
824 \r
825         public void showKeyboard (String type)\r
826         {\r
827             InputMethodManager imm = (InputMethodManager) getSystemService (Context.INPUT_METHOD_SERVICE);\r
828 \r
829             if (imm != null)\r
830             {\r
831                 if (type.length() > 0)\r
832                 {\r
833                     imm.showSoftInput (this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT);\r
834                     imm.setInputMethod (getWindowToken(), type);\r
835                     keyboardDismissListener.startListening();\r
836                 }\r
837                 else\r
838                 {\r
839                     imm.hideSoftInputFromWindow (getWindowToken(), 0);\r
840                     keyboardDismissListener.stopListening();\r
841                 }\r
842             }\r
843         }\r
844 \r
845         public void backButtonPressed()\r
846         {\r
847             if (host == 0)\r
848                 return;\r
849 \r
850             handleBackButton (host);\r
851         }\r
852 \r
853         @Override\r
854         public boolean onKeyDown (int keyCode, KeyEvent event)\r
855         {\r
856             if (host == 0)\r
857                 return false;\r
858 \r
859             switch (keyCode)\r
860             {\r
861                 case KeyEvent.KEYCODE_VOLUME_UP:\r
862                 case KeyEvent.KEYCODE_VOLUME_DOWN:\r
863                     return super.onKeyDown (keyCode, event);\r
864                 case KeyEvent.KEYCODE_BACK:\r
865                 {\r
866                     ((Activity) getContext()).onBackPressed();\r
867                     return true;\r
868                 }\r
869 \r
870                 default:\r
871                     break;\r
872             }\r
873 \r
874             handleKeyDown (host, keyCode, event.getUnicodeChar());\r
875             return true;\r
876         }\r
877 \r
878         @Override\r
879         public boolean onKeyUp (int keyCode, KeyEvent event)\r
880         {\r
881             if (host == 0)\r
882                 return false;\r
883 \r
884             handleKeyUp (host, keyCode, event.getUnicodeChar());\r
885             return true;\r
886         }\r
887 \r
888         @Override\r
889         public boolean onKeyMultiple (int keyCode, int count, KeyEvent event)\r
890         {\r
891             if (host == 0)\r
892                 return false;\r
893 \r
894             if (keyCode != KeyEvent.KEYCODE_UNKNOWN || event.getAction() != KeyEvent.ACTION_MULTIPLE)\r
895                 return super.onKeyMultiple (keyCode, count, event);\r
896 \r
897             if (event.getCharacters() != null)\r
898             {\r
899                 int utf8Char = event.getCharacters().codePointAt (0);\r
900                 handleKeyDown (host, utf8Char, utf8Char);\r
901                 return true;\r
902             }\r
903 \r
904             return false;\r
905         }\r
906 \r
907         //==============================================================================\r
908         private final class KeyboardDismissListener\r
909         {\r
910             public KeyboardDismissListener (ComponentPeerView viewToUse)\r
911             {\r
912                 view = viewToUse;\r
913             }\r
914 \r
915             private void startListening()\r
916             {\r
917                 view.getViewTreeObserver().addOnGlobalLayoutListener(viewTreeObserver);\r
918             }\r
919 \r
920             private void stopListening()\r
921             {\r
922                 view.getViewTreeObserver().removeGlobalOnLayoutListener(viewTreeObserver);\r
923             }\r
924 \r
925             private class TreeObserver implements ViewTreeObserver.OnGlobalLayoutListener\r
926             {\r
927                 TreeObserver()\r
928                 {\r
929                     keyboardShown = false;\r
930                 }\r
931 \r
932                 @Override\r
933                 public void onGlobalLayout()\r
934                 {\r
935                     Rect r = new Rect();\r
936 \r
937                     ViewGroup parentView = (ViewGroup) getParent();\r
938 \r
939                     if (parentView == null)\r
940                         return;\r
941 \r
942                     parentView.getWindowVisibleDisplayFrame (r);\r
943 \r
944                     int diff = parentView.getHeight() - (r.bottom - r.top);\r
945 \r
946                     // Arbitrary threshold, surely keyboard would take more than 20 pix.\r
947                     if (diff < 20 && keyboardShown)\r
948                     {\r
949                         keyboardShown = false;\r
950                         handleKeyboardHidden (view.host);\r
951                     }\r
952 \r
953                     if (! keyboardShown && diff > 20)\r
954                         keyboardShown = true;\r
955                 };\r
956 \r
957                 private boolean keyboardShown;\r
958             };\r
959 \r
960             private ComponentPeerView view;\r
961             private TreeObserver viewTreeObserver = new TreeObserver();\r
962         }\r
963 \r
964         private KeyboardDismissListener keyboardDismissListener = new KeyboardDismissListener(this);\r
965 \r
966         // this is here to make keyboard entry work on a Galaxy Tab2 10.1\r
967         @Override\r
968         public InputConnection onCreateInputConnection (EditorInfo outAttrs)\r
969         {\r
970             outAttrs.actionLabel = "";\r
971             outAttrs.hintText = "";\r
972             outAttrs.initialCapsMode = 0;\r
973             outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;\r
974             outAttrs.label = "";\r
975             outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI;\r
976             outAttrs.inputType = InputType.TYPE_NULL;\r
977 \r
978             return new BaseInputConnection (this, false);\r
979         }\r
980 \r
981         //==============================================================================\r
982         @Override\r
983         protected void onSizeChanged (int w, int h, int oldw, int oldh)\r
984         {\r
985             if (host == 0)\r
986                 return;\r
987 \r
988             super.onSizeChanged (w, h, oldw, oldh);\r
989             viewSizeChanged (host);\r
990         }\r
991 \r
992         @Override\r
993         protected void onLayout (boolean changed, int left, int top, int right, int bottom)\r
994         {\r
995             for (int i = getChildCount(); --i >= 0;)\r
996                 requestTransparentRegion (getChildAt (i));\r
997         }\r
998 \r
999         private native void viewSizeChanged (long host);\r
1000 \r
1001         @Override\r
1002         public void onFocusChange (View v, boolean hasFocus)\r
1003         {\r
1004             if (host == 0)\r
1005                 return;\r
1006 \r
1007             if (v == this)\r
1008                 focusChanged (host, hasFocus);\r
1009         }\r
1010 \r
1011         private native void focusChanged (long host, boolean hasFocus);\r
1012 \r
1013         public void setViewName (String newName)    {}\r
1014 \r
1015         public void setSystemUiVisibilityCompat (int visibility)\r
1016         {\r
1017             Method systemUIVisibilityMethod = null;\r
1018             try\r
1019             {\r
1020                 systemUIVisibilityMethod = this.getClass().getMethod ("setSystemUiVisibility", int.class);\r
1021             }\r
1022             catch (SecurityException e)     { return; }\r
1023             catch (NoSuchMethodException e) { return; }\r
1024             if (systemUIVisibilityMethod == null) return;\r
1025 \r
1026             try\r
1027             {\r
1028                 systemUIVisibilityMethod.invoke (this, visibility);\r
1029             }\r
1030             catch (java.lang.IllegalArgumentException e) {}\r
1031             catch (java.lang.IllegalAccessException e) {}\r
1032             catch (java.lang.reflect.InvocationTargetException e) {}\r
1033         }\r
1034 \r
1035         public boolean isVisible()                  { return getVisibility() == VISIBLE; }\r
1036         public void setVisible (boolean b)          { setVisibility (b ? VISIBLE : INVISIBLE); }\r
1037 \r
1038         public boolean containsPoint (int x, int y)\r
1039         {\r
1040             return true; //xxx needs to check overlapping views\r
1041         }\r
1042 \r
1043         //==============================================================================\r
1044         private native void handleAppPaused (long host);\r
1045         private native void handleAppResumed (long host);\r
1046 \r
1047         @Override\r
1048         public void appPaused()\r
1049         {\r
1050             if (host == 0)\r
1051                 return;\r
1052 \r
1053             handleAppPaused (host);\r
1054         }\r
1055 \r
1056         @Override\r
1057         public void appResumed()\r
1058         {\r
1059             if (host == 0)\r
1060                 return;\r
1061 \r
1062             // Ensure that navigation/status bar visibility is correctly restored.\r
1063             handleAppResumed (host);\r
1064         }\r
1065     }\r
1066 \r
1067     //==============================================================================\r
1068     public static class NativeSurfaceView    extends SurfaceView\r
1069                                           implements SurfaceHolder.Callback\r
1070     {\r
1071         private long nativeContext = 0;\r
1072         private boolean forVideo;\r
1073 \r
1074         NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo)\r
1075         {\r
1076             super (context);\r
1077             nativeContext = nativeContextPtr;\r
1078             forVideo = createdForVideo;\r
1079         }\r
1080 \r
1081         public Surface getNativeSurface()\r
1082         {\r
1083             Surface retval = null;\r
1084 \r
1085             SurfaceHolder holder = getHolder();\r
1086             if (holder != null)\r
1087                 retval = holder.getSurface();\r
1088 \r
1089             return retval;\r
1090         }\r
1091 \r
1092         //==============================================================================\r
1093         @Override\r
1094         public void surfaceChanged (SurfaceHolder holder, int format, int width, int height)\r
1095         {\r
1096             if (forVideo)\r
1097                 surfaceChangedNativeVideo (nativeContext, holder, format, width, height);\r
1098             else\r
1099                 surfaceChangedNative (nativeContext, holder, format, width, height);\r
1100         }\r
1101 \r
1102         @Override\r
1103         public void surfaceCreated (SurfaceHolder holder)\r
1104         {\r
1105             if (forVideo)\r
1106                 surfaceCreatedNativeVideo (nativeContext, holder);\r
1107             else\r
1108                 surfaceCreatedNative (nativeContext, holder);\r
1109         }\r
1110 \r
1111         @Override\r
1112         public void surfaceDestroyed (SurfaceHolder holder)\r
1113         {\r
1114             if (forVideo)\r
1115                 surfaceDestroyedNativeVideo (nativeContext, holder);\r
1116             else\r
1117                 surfaceDestroyedNative (nativeContext, holder);\r
1118         }\r
1119 \r
1120         @Override\r
1121         protected void dispatchDraw (Canvas canvas)\r
1122         {\r
1123             super.dispatchDraw (canvas);\r
1124 \r
1125             if (forVideo)\r
1126                 dispatchDrawNativeVideo (nativeContext, canvas);\r
1127             else\r
1128                 dispatchDrawNative (nativeContext, canvas);\r
1129         }\r
1130 \r
1131         //==============================================================================\r
1132         @Override\r
1133         protected void onAttachedToWindow()\r
1134         {\r
1135             super.onAttachedToWindow();\r
1136             getHolder().addCallback (this);\r
1137         }\r
1138 \r
1139         @Override\r
1140         protected void onDetachedFromWindow()\r
1141         {\r
1142             super.onDetachedFromWindow();\r
1143             getHolder().removeCallback (this);\r
1144         }\r
1145 \r
1146         //==============================================================================\r
1147         private native void dispatchDrawNative (long nativeContextPtr, Canvas canvas);\r
1148         private native void surfaceCreatedNative (long nativeContextptr, SurfaceHolder holder);\r
1149         private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder);\r
1150         private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder,\r
1151                                                   int format, int width, int height);\r
1152 \r
1153         private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas);\r
1154         private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder);\r
1155         private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder);\r
1156         private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder,\r
1157                                                        int format, int width, int height);\r
1158     }\r
1159 \r
1160     public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo)\r
1161     {\r
1162         return new NativeSurfaceView (this, nativeSurfacePtr, forVideo);\r
1163     }\r
1164 \r
1165     //==============================================================================\r
1166     public final int[] renderGlyph (char glyph1, char glyph2, Paint paint, android.graphics.Matrix matrix, Rect bounds)\r
1167     {\r
1168         Path p = new Path();\r
1169 \r
1170         char[] str = { glyph1, glyph2 };\r
1171         paint.getTextPath (str, 0, (glyph2 != 0 ? 2 : 1), 0.0f, 0.0f, p);\r
1172 \r
1173         RectF boundsF = new RectF();\r
1174         p.computeBounds (boundsF, true);\r
1175         matrix.mapRect (boundsF);\r
1176 \r
1177         boundsF.roundOut (bounds);\r
1178         bounds.left--;\r
1179         bounds.right++;\r
1180 \r
1181         final int w = bounds.width();\r
1182         final int h = Math.max (1, bounds.height());\r
1183 \r
1184         Bitmap bm = Bitmap.createBitmap (w, h, Bitmap.Config.ARGB_8888);\r
1185 \r
1186         Canvas c = new Canvas (bm);\r
1187         matrix.postTranslate (-bounds.left, -bounds.top);\r
1188         c.setMatrix (matrix);\r
1189         c.drawPath (p, paint);\r
1190 \r
1191         final int sizeNeeded = w * h;\r
1192         if (cachedRenderArray.length < sizeNeeded)\r
1193             cachedRenderArray = new int [sizeNeeded];\r
1194 \r
1195         bm.getPixels (cachedRenderArray, 0, w, 0, 0, w, h);\r
1196         bm.recycle();\r
1197         return cachedRenderArray;\r
1198     }\r
1199 \r
1200     private int[] cachedRenderArray = new int [256];\r
1201 \r
1202     //==============================================================================\r
1203     public static class NativeInvocationHandler implements InvocationHandler\r
1204     {\r
1205         public NativeInvocationHandler (Activity activityToUse, long nativeContextRef)\r
1206         {\r
1207             activity = activityToUse;\r
1208             nativeContext = nativeContextRef;\r
1209         }\r
1210 \r
1211         public void nativeContextDeleted()\r
1212         {\r
1213             nativeContext = 0;\r
1214         }\r
1215 \r
1216         @Override\r
1217         public void finalize()\r
1218         {\r
1219             activity.runOnUiThread (new Runnable()\r
1220                                     {\r
1221                                         @Override\r
1222                                         public void run()\r
1223                                         {\r
1224                                             if (nativeContext != 0)\r
1225                                                 dispatchFinalize (nativeContext);\r
1226                                         }\r
1227                                     });\r
1228         }\r
1229 \r
1230         @Override\r
1231         public Object invoke (Object proxy, Method method, Object[] args) throws Throwable\r
1232         {\r
1233             return dispatchInvoke (nativeContext, proxy, method, args);\r
1234         }\r
1235 \r
1236         //==============================================================================\r
1237         Activity activity;\r
1238         private long nativeContext = 0;\r
1239 \r
1240         private native void dispatchFinalize (long nativeContextRef);\r
1241         private native Object dispatchInvoke (long nativeContextRef, Object proxy, Method method, Object[] args);\r
1242     }\r
1243 \r
1244     public InvocationHandler createInvocationHandler (long nativeContextRef)\r
1245     {\r
1246         return new NativeInvocationHandler (this, nativeContextRef);\r
1247     }\r
1248 \r
1249     public void invocationHandlerContextDeleted (InvocationHandler handler)\r
1250     {\r
1251         ((NativeInvocationHandler) handler).nativeContextDeleted();\r
1252     }\r
1253 \r
1254     //==============================================================================\r
1255     public static class HTTPStream\r
1256     {\r
1257         public HTTPStream (String address, boolean isPostToUse, byte[] postDataToUse,\r
1258                            String headersToUse, int timeOutMsToUse,\r
1259                            int[] statusCodeToUse, StringBuffer responseHeadersToUse,\r
1260                            int numRedirectsToFollowToUse, String httpRequestCmdToUse) throws IOException\r
1261         {\r
1262             isPost = isPostToUse;\r
1263             postData = postDataToUse;\r
1264             headers = headersToUse;\r
1265             timeOutMs = timeOutMsToUse;\r
1266             statusCode = statusCodeToUse;\r
1267             responseHeaders = responseHeadersToUse;\r
1268             totalLength = -1;\r
1269             numRedirectsToFollow = numRedirectsToFollowToUse;\r
1270             httpRequestCmd = httpRequestCmdToUse;\r
1271 \r
1272             connection = createConnection (address, isPost, postData, headers, timeOutMs, httpRequestCmd);\r
1273         }\r
1274 \r
1275         private final HttpURLConnection createConnection (String address, boolean isPost, byte[] postData,\r
1276                                                           String headers, int timeOutMs, String httpRequestCmdToUse) throws IOException\r
1277         {\r
1278             HttpURLConnection newConnection = (HttpURLConnection) (new URL(address).openConnection());\r
1279 \r
1280             try\r
1281             {\r
1282                 newConnection.setInstanceFollowRedirects (false);\r
1283                 newConnection.setConnectTimeout (timeOutMs);\r
1284                 newConnection.setReadTimeout (timeOutMs);\r
1285 \r
1286                 // headers - if not empty, this string is appended onto the headers that are used for the request. It must therefore be a valid set of HTML header directives, separated by newlines.\r
1287                 // So convert headers string to an array, with an element for each line\r
1288                 String headerLines[] = headers.split("\\n");\r
1289 \r
1290                 // Set request headers\r
1291                 for (int i = 0; i < headerLines.length; ++i)\r
1292                 {\r
1293                     int pos = headerLines[i].indexOf (":");\r
1294 \r
1295                     if (pos > 0 && pos < headerLines[i].length())\r
1296                     {\r
1297                         String field = headerLines[i].substring (0, pos);\r
1298                         String value = headerLines[i].substring (pos + 1);\r
1299 \r
1300                         if (value.length() > 0)\r
1301                             newConnection.setRequestProperty (field, value);\r
1302                     }\r
1303                 }\r
1304 \r
1305                 newConnection.setRequestMethod (httpRequestCmd);\r
1306 \r
1307                 if (isPost)\r
1308                 {\r
1309                     newConnection.setDoOutput (true);\r
1310 \r
1311                     if (postData != null)\r
1312                     {\r
1313                         OutputStream out = newConnection.getOutputStream();\r
1314                         out.write(postData);\r
1315                         out.flush();\r
1316                     }\r
1317                 }\r
1318 \r
1319                 return newConnection;\r
1320             }\r
1321             catch (Throwable e)\r
1322             {\r
1323                 newConnection.disconnect();\r
1324                 throw new IOException ("Connection error");\r
1325             }\r
1326         }\r
1327 \r
1328         private final InputStream getCancellableStream (final boolean isInput) throws ExecutionException\r
1329         {\r
1330             synchronized (createFutureLock)\r
1331             {\r
1332                 if (hasBeenCancelled.get())\r
1333                     return null;\r
1334 \r
1335                 streamFuture = executor.submit (new Callable<BufferedInputStream>()\r
1336                 {\r
1337                     @Override\r
1338                     public BufferedInputStream call() throws IOException\r
1339                     {\r
1340                         return new BufferedInputStream (isInput ? connection.getInputStream()\r
1341                                                                 : connection.getErrorStream());\r
1342                     }\r
1343                 });\r
1344             }\r
1345 \r
1346             try\r
1347             {\r
1348                 return streamFuture.get();\r
1349             }\r
1350             catch (InterruptedException e)\r
1351             {\r
1352                 return null;\r
1353             }\r
1354             catch (CancellationException e)\r
1355             {\r
1356                 return null;\r
1357             }\r
1358         }\r
1359 \r
1360         public final boolean connect()\r
1361         {\r
1362             boolean result = false;\r
1363             int numFollowedRedirects = 0;\r
1364 \r
1365             while (true)\r
1366             {\r
1367                 result = doConnect();\r
1368 \r
1369                 if (! result)\r
1370                     return false;\r
1371 \r
1372                 if (++numFollowedRedirects > numRedirectsToFollow)\r
1373                     break;\r
1374 \r
1375                 int status = statusCode[0];\r
1376 \r
1377                 if (status == 301 || status == 302 || status == 303 || status == 307)\r
1378                 {\r
1379                     // Assumes only one occurrence of "Location"\r
1380                     int pos1 = responseHeaders.indexOf ("Location:") + 10;\r
1381                     int pos2 = responseHeaders.indexOf ("\n", pos1);\r
1382 \r
1383                     if (pos2 > pos1)\r
1384                     {\r
1385                         String currentLocation = connection.getURL().toString();\r
1386                         String newLocation = responseHeaders.substring (pos1, pos2);\r
1387 \r
1388                         try\r
1389                         {\r
1390                             // Handle newLocation whether it's absolute or relative\r
1391                             URL baseUrl = new URL (currentLocation);\r
1392                             URL newUrl  = new URL (baseUrl, newLocation);\r
1393                             String transformedNewLocation = newUrl.toString();\r
1394 \r
1395                             if (transformedNewLocation != currentLocation)\r
1396                             {\r
1397                                 // Clear responseHeaders before next iteration\r
1398                                 responseHeaders.delete (0, responseHeaders.length());\r
1399 \r
1400                                 synchronized (createStreamLock)\r
1401                                 {\r
1402                                     if (hasBeenCancelled.get())\r
1403                                         return false;\r
1404 \r
1405                                     connection.disconnect();\r
1406 \r
1407                                     try\r
1408                                     {\r
1409                                         connection = createConnection (transformedNewLocation, isPost,\r
1410                                                                        postData, headers, timeOutMs,\r
1411                                                                        httpRequestCmd);\r
1412                                     }\r
1413                                     catch (Throwable e)\r
1414                                     {\r
1415                                         return false;\r
1416                                     }\r
1417                                 }\r
1418                             }\r
1419                             else\r
1420                             {\r
1421                                 break;\r
1422                             }\r
1423                         }\r
1424                         catch (Throwable e)\r
1425                         {\r
1426                             return false;\r
1427                         }\r
1428                     }\r
1429                     else\r
1430                     {\r
1431                         break;\r
1432                     }\r
1433                 }\r
1434                 else\r
1435                 {\r
1436                     break;\r
1437                 }\r
1438             }\r
1439 \r
1440             return result;\r
1441         }\r
1442 \r
1443         private final boolean doConnect()\r
1444         {\r
1445             synchronized (createStreamLock)\r
1446             {\r
1447                 if (hasBeenCancelled.get())\r
1448                     return false;\r
1449 \r
1450                 try\r
1451                 {\r
1452                     try\r
1453                     {\r
1454                         inputStream = getCancellableStream (true);\r
1455                     }\r
1456                     catch (ExecutionException e)\r
1457                     {\r
1458                         if (connection.getResponseCode() < 400)\r
1459                         {\r
1460                             statusCode[0] = connection.getResponseCode();\r
1461                             connection.disconnect();\r
1462                             return false;\r
1463                         }\r
1464                     }\r
1465                     finally\r
1466                     {\r
1467                         statusCode[0] = connection.getResponseCode();\r
1468                     }\r
1469 \r
1470                     try\r
1471                     {\r
1472                         if (statusCode[0] >= 400)\r
1473                             inputStream = getCancellableStream (false);\r
1474                         else\r
1475                             inputStream = getCancellableStream (true);\r
1476                     }\r
1477                     catch (ExecutionException e)\r
1478                     {}\r
1479 \r
1480                     for (java.util.Map.Entry<String, java.util.List<String>> entry : connection.getHeaderFields().entrySet())\r
1481                     {\r
1482                         if (entry.getKey() != null && entry.getValue() != null)\r
1483                         {\r
1484                             responseHeaders.append(entry.getKey() + ": "\r
1485                                                    + android.text.TextUtils.join(",", entry.getValue()) + "\n");\r
1486 \r
1487                             if (entry.getKey().compareTo ("Content-Length") == 0)\r
1488                                 totalLength = Integer.decode (entry.getValue().get (0));\r
1489                         }\r
1490                     }\r
1491 \r
1492                     return true;\r
1493                 }\r
1494                 catch (IOException e)\r
1495                 {\r
1496                     return false;\r
1497                 }\r
1498             }\r
1499         }\r
1500 \r
1501         static class DisconnectionRunnable implements Runnable\r
1502         {\r
1503             public DisconnectionRunnable (HttpURLConnection theConnection,\r
1504                                           InputStream theInputStream,\r
1505                                           ReentrantLock theCreateStreamLock,\r
1506                                           Object theCreateFutureLock,\r
1507                                           Future<BufferedInputStream> theStreamFuture)\r
1508             {\r
1509                 connectionToDisconnect = theConnection;\r
1510                 inputStream = theInputStream;\r
1511                 createStreamLock = theCreateStreamLock;\r
1512                 createFutureLock = theCreateFutureLock;\r
1513                 streamFuture = theStreamFuture;\r
1514             }\r
1515 \r
1516             public void run()\r
1517             {\r
1518                 try\r
1519                 {\r
1520                     if (! createStreamLock.tryLock())\r
1521                     {\r
1522                         synchronized (createFutureLock)\r
1523                         {\r
1524                             if (streamFuture != null)\r
1525                                 streamFuture.cancel (true);\r
1526                         }\r
1527 \r
1528                         createStreamLock.lock();\r
1529                     }\r
1530 \r
1531                     if (connectionToDisconnect != null)\r
1532                         connectionToDisconnect.disconnect();\r
1533 \r
1534                     if (inputStream != null)\r
1535                         inputStream.close();\r
1536                 }\r
1537                 catch (IOException e)\r
1538                 {}\r
1539                 finally\r
1540                 {\r
1541                     createStreamLock.unlock();\r
1542                 }\r
1543             }\r
1544 \r
1545             private HttpURLConnection connectionToDisconnect;\r
1546             private InputStream inputStream;\r
1547             private ReentrantLock createStreamLock;\r
1548             private Object createFutureLock;\r
1549             Future<BufferedInputStream> streamFuture;\r
1550         }\r
1551 \r
1552         public final void release()\r
1553         {\r
1554             DisconnectionRunnable disconnectionRunnable = new DisconnectionRunnable (connection,\r
1555                                                                                      inputStream,\r
1556                                                                                      createStreamLock,\r
1557                                                                                      createFutureLock,\r
1558                                                                                      streamFuture);\r
1559 \r
1560             synchronized (createStreamLock)\r
1561             {\r
1562                 hasBeenCancelled.set (true);\r
1563 \r
1564                 connection = null;\r
1565             }\r
1566 \r
1567             Thread disconnectionThread = new Thread(disconnectionRunnable);\r
1568             disconnectionThread.start();\r
1569         }\r
1570 \r
1571         public final int read (byte[] buffer, int numBytes)\r
1572         {\r
1573             int num = 0;\r
1574 \r
1575             try\r
1576             {\r
1577                 synchronized (createStreamLock)\r
1578                 {\r
1579                     if (inputStream != null)\r
1580                         num = inputStream.read (buffer, 0, numBytes);\r
1581                 }\r
1582             }\r
1583             catch (IOException e)\r
1584             {}\r
1585 \r
1586             if (num > 0)\r
1587                 position += num;\r
1588 \r
1589             return num;\r
1590         }\r
1591 \r
1592         public final long getPosition()                 { return position; }\r
1593         public final long getTotalLength()              { return totalLength; }\r
1594         public final boolean isExhausted()              { return false; }\r
1595         public final boolean setPosition (long newPos)  { return false; }\r
1596 \r
1597         private boolean isPost;\r
1598         private byte[] postData;\r
1599         private String headers;\r
1600         private int timeOutMs;\r
1601         String httpRequestCmd;\r
1602         private HttpURLConnection connection;\r
1603         private int[] statusCode;\r
1604         private StringBuffer responseHeaders;\r
1605         private int totalLength;\r
1606         private int numRedirectsToFollow;\r
1607         private InputStream inputStream;\r
1608         private long position;\r
1609         private final ReentrantLock createStreamLock = new ReentrantLock();\r
1610         private final Object createFutureLock = new Object();\r
1611         private AtomicBoolean hasBeenCancelled = new AtomicBoolean();\r
1612 \r
1613         private final ExecutorService executor = Executors.newCachedThreadPool (Executors.defaultThreadFactory());\r
1614         Future<BufferedInputStream> streamFuture;\r
1615     }\r
1616 \r
1617     public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData,\r
1618                                                      String headers, int timeOutMs, int[] statusCode,\r
1619                                                      StringBuffer responseHeaders, int numRedirectsToFollow,\r
1620                                                      String httpRequestCmd)\r
1621     {\r
1622         // timeout parameter of zero for HttpUrlConnection is a blocking connect (negative value for juce::URL)\r
1623         if (timeOutMs < 0)\r
1624             timeOutMs = 0;\r
1625         else if (timeOutMs == 0)\r
1626             timeOutMs = 30000;\r
1627 \r
1628         for (;;)\r
1629         {\r
1630             try\r
1631             {\r
1632                 HTTPStream httpStream = new HTTPStream (address, isPost, postData, headers,\r
1633                                                         timeOutMs, statusCode, responseHeaders,\r
1634                                                         numRedirectsToFollow, httpRequestCmd);\r
1635 \r
1636                 return httpStream;\r
1637             }\r
1638             catch (Throwable e) {}\r
1639 \r
1640             return null;\r
1641         }\r
1642     }\r
1643 \r
1644     public final void launchURL (String url)\r
1645     {\r
1646         startActivity (new Intent (Intent.ACTION_VIEW, Uri.parse (url)));\r
1647     }\r
1648 \r
1649     private native boolean webViewPageLoadStarted (long host, WebView view, String url);\r
1650     private native void webViewPageLoadFinished (long host, WebView view, String url);\r
1651     private native void webViewReceivedSslError (long host, WebView view, SslErrorHandler handler, SslError error);\r
1652     private native void webViewCloseWindowRequest (long host, WebView view);\r
1653     private native void webViewCreateWindowRequest (long host, WebView view);\r
1654 \r
1655     //==============================================================================\r
1656     public class JuceWebViewClient   extends WebViewClient\r
1657     {\r
1658         public JuceWebViewClient (long hostToUse)\r
1659         {\r
1660             host = hostToUse;\r
1661         }\r
1662 \r
1663         public void hostDeleted()\r
1664         {\r
1665             synchronized (hostLock)\r
1666             {\r
1667                 host = 0;\r
1668             }\r
1669         }\r
1670 \r
1671         @Override\r
1672         public void onPageFinished (WebView view, String url)\r
1673         {\r
1674             if (host == 0)\r
1675                 return;\r
1676 \r
1677             webViewPageLoadFinished (host, view, url);\r
1678         }\r
1679 \r
1680         @Override\r
1681         public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error)\r
1682         {\r
1683             if (host == 0)\r
1684                 return;\r
1685 \r
1686             webViewReceivedSslError (host, view, handler, error);\r
1687         }\r
1688 \r
1689         @Override\r
1690         public void onPageStarted (WebView view, String url, Bitmap favicon)\r
1691         {\r
1692             if (host != 0)\r
1693                 webViewPageLoadStarted (host, view, url);\r
1694         }\r
1695 \r
1696         private long host;\r
1697         private final Object hostLock = new Object();\r
1698     }\r
1699 \r
1700     public class JuceWebChromeClient    extends WebChromeClient\r
1701     {\r
1702         public JuceWebChromeClient (long hostToUse)\r
1703         {\r
1704             host = hostToUse;\r
1705         }\r
1706 \r
1707         @Override\r
1708         public void onCloseWindow (WebView window)\r
1709         {\r
1710             webViewCloseWindowRequest (host, window);\r
1711         }\r
1712 \r
1713         @Override\r
1714         public boolean onCreateWindow (WebView view, boolean isDialog,\r
1715                                        boolean isUserGesture, Message resultMsg)\r
1716         {\r
1717             webViewCreateWindowRequest (host, view);\r
1718             return false;\r
1719         }\r
1720 \r
1721         private long host;\r
1722         private final Object hostLock = new Object();\r
1723     }\r
1724 \r
1725 \r
1726     //==============================================================================\r
1727     public static final String getLocaleValue (boolean isRegion)\r
1728     {\r
1729         java.util.Locale locale = java.util.Locale.getDefault();\r
1730 \r
1731         return isRegion ? locale.getCountry()\r
1732                         : locale.getLanguage();\r
1733     }\r
1734 \r
1735     private static final String getFileLocation (String type)\r
1736     {\r
1737         return Environment.getExternalStoragePublicDirectory (type).getAbsolutePath();\r
1738     }\r
1739 \r
1740     public static final String getDocumentsFolder()\r
1741     {\r
1742         if (getAndroidSDKVersion() >= 19)\r
1743             return getFileLocation ("Documents");\r
1744 \r
1745         return Environment.getDataDirectory().getAbsolutePath();\r
1746     }\r
1747 \r
1748     public static final String getPicturesFolder()   { return getFileLocation (Environment.DIRECTORY_PICTURES); }\r
1749     public static final String getMusicFolder()      { return getFileLocation (Environment.DIRECTORY_MUSIC); }\r
1750     public static final String getMoviesFolder()     { return getFileLocation (Environment.DIRECTORY_MOVIES); }\r
1751     public static final String getDownloadsFolder()  { return getFileLocation (Environment.DIRECTORY_DOWNLOADS); }\r
1752 \r
1753     //==============================================================================\r
1754     @Override\r
1755     protected void onActivityResult (int requestCode, int resultCode, Intent data)\r
1756     {\r
1757         appActivityResult (requestCode, resultCode, data);\r
1758     }\r
1759 \r
1760     @Override\r
1761     protected void onNewIntent (Intent intent)\r
1762     {\r
1763         super.onNewIntent(intent);\r
1764         setIntent(intent);\r
1765 \r
1766         appNewIntent (intent);\r
1767     }\r
1768 \r
1769     //==============================================================================\r
1770     public final Typeface getTypeFaceFromAsset (String assetName)\r
1771     {\r
1772         try\r
1773         {\r
1774             return Typeface.createFromAsset (this.getResources().getAssets(), assetName);\r
1775         }\r
1776         catch (Throwable e) {}\r
1777 \r
1778         return null;\r
1779     }\r
1780 \r
1781     final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();\r
1782 \r
1783     public static String bytesToHex (byte[] bytes)\r
1784     {\r
1785         char[] hexChars = new char[bytes.length * 2];\r
1786 \r
1787         for (int j = 0; j < bytes.length; ++j)\r
1788         {\r
1789             int v = bytes[j] & 0xff;\r
1790             hexChars[j * 2]     = hexArray[v >>> 4];\r
1791             hexChars[j * 2 + 1] = hexArray[v & 0x0f];\r
1792         }\r
1793 \r
1794         return new String (hexChars);\r
1795     }\r
1796 \r
1797     final private java.util.Map dataCache = new java.util.HashMap();\r
1798 \r
1799     synchronized private final File getDataCacheFile (byte[] data)\r
1800     {\r
1801         try\r
1802         {\r
1803             java.security.MessageDigest digest = java.security.MessageDigest.getInstance ("MD5");\r
1804             digest.update (data);\r
1805 \r
1806             String key = bytesToHex (digest.digest());\r
1807 \r
1808             if (dataCache.containsKey (key))\r
1809                 return (File) dataCache.get (key);\r
1810 \r
1811             File f = new File (this.getCacheDir(), "bindata_" + key);\r
1812             f.delete();\r
1813             FileOutputStream os = new FileOutputStream (f);\r
1814             os.write (data, 0, data.length);\r
1815             dataCache.put (key, f);\r
1816             return f;\r
1817         }\r
1818         catch (Throwable e) {}\r
1819 \r
1820         return null;\r
1821     }\r
1822 \r
1823     private final void clearDataCache()\r
1824     {\r
1825         java.util.Iterator it = dataCache.values().iterator();\r
1826 \r
1827         while (it.hasNext())\r
1828         {\r
1829             File f = (File) it.next();\r
1830             f.delete();\r
1831         }\r
1832     }\r
1833 \r
1834     public final Typeface getTypeFaceFromByteArray (byte[] data)\r
1835     {\r
1836         try\r
1837         {\r
1838             File f = getDataCacheFile (data);\r
1839 \r
1840             if (f != null)\r
1841                 return Typeface.createFromFile (f);\r
1842         }\r
1843         catch (Exception e)\r
1844         {\r
1845             Log.e ("JUCE", e.toString());\r
1846         }\r
1847 \r
1848         return null;\r
1849     }\r
1850 \r
1851     public static final int getAndroidSDKVersion()\r
1852     {\r
1853         return android.os.Build.VERSION.SDK_INT;\r
1854     }\r
1855 \r
1856     public final String audioManagerGetProperty (String property)\r
1857     {\r
1858         Object obj = getSystemService (AUDIO_SERVICE);\r
1859         if (obj == null)\r
1860             return null;\r
1861 \r
1862         java.lang.reflect.Method method;\r
1863 \r
1864         try\r
1865         {\r
1866             method = obj.getClass().getMethod ("getProperty", String.class);\r
1867         }\r
1868         catch (SecurityException e)     { return null; }\r
1869         catch (NoSuchMethodException e) { return null; }\r
1870 \r
1871         if (method == null)\r
1872             return null;\r
1873 \r
1874         try\r
1875         {\r
1876             return (String) method.invoke (obj, property);\r
1877         }\r
1878         catch (java.lang.IllegalArgumentException e) {}\r
1879         catch (java.lang.IllegalAccessException e) {}\r
1880         catch (java.lang.reflect.InvocationTargetException e) {}\r
1881 \r
1882         return null;\r
1883     }\r
1884 \r
1885     public final boolean hasSystemFeature (String property)\r
1886     {\r
1887         return getPackageManager().hasSystemFeature (property);\r
1888     }\r
1889 }\r